Tiếng Việt

Khám phá các nguyên tắc cơ bản của Cây Tìm Kiếm Nhị Phân (BST) và học cách triển khai hiệu quả bằng JavaScript. Hướng dẫn này bao gồm cấu trúc, phép toán và ví dụ thực tế.

Cây Tìm Kiếm Nhị Phân: Hướng Dẫn Triển Khai Toàn Diện Bằng JavaScript

Cây Tìm Kiếm Nhị Phân (BST) là một cấu trúc dữ liệu cơ bản trong khoa học máy tính, được sử dụng rộng rãi để tìm kiếm, sắp xếp và truy xuất dữ liệu hiệu quả. Cấu trúc phân cấp của chúng cho phép độ phức tạp thời gian logarit trong nhiều phép toán, khiến chúng trở thành một công cụ mạnh mẽ để quản lý các tập dữ liệu lớn. Hướng dẫn này cung cấp một cái nhìn tổng quan toàn diện về BST và trình bày cách triển khai chúng trong JavaScript, phục vụ cho các nhà phát triển trên toàn thế giới.

Tìm Hiểu về Cây Tìm Kiếm Nhị Phân

Cây Tìm Kiếm Nhị Phân là gì?

Cây Tìm Kiếm Nhị Phân là một cấu trúc dữ liệu dựa trên cây, trong đó mỗi nút có tối đa hai con, được gọi là con trái và con phải. Thuộc tính chính của một BST là đối với bất kỳ nút nào:

Thuộc tính này đảm bảo rằng các phần tử trong BST luôn được sắp xếp, cho phép tìm kiếm và truy xuất hiệu quả.

Các Khái Niệm Chính

Triển Khai Cây Tìm Kiếm Nhị Phân Bằng JavaScript

Định Nghĩa Lớp Node

Đầu tiên, chúng ta định nghĩa một lớp `Node` để biểu diễn mỗi nút trong BST. Mỗi nút sẽ chứa một `key` để lưu trữ dữ liệu và các con trỏ `left` và `right` đến các con của nó.


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

Định Nghĩa Lớp Cây Tìm Kiếm Nhị Phân

Tiếp theo, chúng ta định nghĩa lớp `BinarySearchTree`. Lớp này sẽ chứa nút gốc và các phương thức để chèn, tìm kiếm, xóa và duyệt cây.


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // Các phương thức sẽ được thêm vào đây
}

Chèn (Insertion)

Phương thức `insert` thêm một nút mới với khóa đã cho vào BST. Quá trình chèn duy trì thuộc tính của BST bằng cách đặt nút mới vào vị trí thích hợp so với các nút hiện có.


insert(key) {
  const newNode = new Node(key);

  if (this.root === null) {
    this.root = newNode;
  } else {
    this.insertNode(this.root, newNode);
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

Ví dụ: Chèn các giá trị vào BST


const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

Tìm Kiếm (Searching)

Phương thức `search` kiểm tra xem một nút với khóa đã cho có tồn tại trong BST hay không. Nó duyệt qua cây, so sánh khóa với khóa của nút hiện tại và di chuyển đến cây con trái hoặc phải tương ứng.


search(key) {
  return this.searchNode(this.root, key);
}

searchNode(node, key) {
  if (node === null) {
    return false;
  }

  if (key < node.key) {
    return this.searchNode(node.left, key);
  } else if (key > node.key) {
    return this.searchNode(node.right, key);
  } else {
    return true;
  }
}

Ví dụ: Tìm kiếm một giá trị trong BST


console.log(bst.search(9));  // Output: true
console.log(bst.search(2));  // Output: false

Xóa (Deletion)

Phương thức `remove` xóa một nút với khóa đã cho khỏi BST. Đây là phép toán phức tạp nhất vì nó cần duy trì thuộc tính của BST trong khi xóa nút. Có ba trường hợp cần xem xét:


remove(key) {
  this.root = this.removeNode(this.root, key);
}

removeNode(node, key) {
  if (node === null) {
    return null;
  }

  if (key < node.key) {
    node.left = this.removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = this.removeNode(node.right, key);
    return node;
  } else {
    // khóa bằng với khóa của nút

    // trường hợp 1 - một nút lá
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // trường hợp 2 - nút chỉ có 1 con
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // trường hợp 3 - nút có 2 con
    const aux = this.findMinNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
  }
}

findMinNode(node) {
  let current = node;
  while (current != null && current.left != null) {
    current = current.left;
  }
  return current;
}

Ví dụ: Xóa một giá trị khỏi BST


bst.remove(7);
console.log(bst.search(7)); // Output: false

Duyệt Cây (Tree Traversal)

Duyệt cây bao gồm việc ghé thăm mỗi nút trong cây theo một thứ tự cụ thể. Có một số phương pháp duyệt phổ biến:


inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

preOrderTraverseNode(node, callback) {
  if (node !== null) {
    callback(node.key);
    this.preOrderTraverseNode(node.left, callback);
    this.preOrderTraverseNode(node.right, callback);
  }
}

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

postOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.postOrderTraverseNode(node.left, callback);
    this.postOrderTraverseNode(node.right, callback);
    callback(node.key);
  }
}

Ví dụ: Duyệt BST


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // Output: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // Output: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Output: 3 8 10 9 12 14 13 18 25 20 15 11

Giá Trị Tối Thiểu và Tối Đa

Việc tìm giá trị tối thiểu và tối đa trong BST rất đơn giản nhờ vào tính chất được sắp xếp của nó.


min() {
  return this.minNode(this.root);
}

minNode(node) {
  let current = node;
  while (current !== null && current.left !== null) {
    current = current.left;
  }
  return current;
}

max() {
  return this.maxNode(this.root);
}

maxNode(node) {
  let current = node;
  while (current !== null && current.right !== null) {
    current = current.right;
  }
  return current;
}

Ví dụ: Tìm giá trị tối thiểu và tối đa


console.log(bst.min().key); // Output: 3
console.log(bst.max().key); // Output: 25

Ứng Dụng Thực Tế của Cây Tìm Kiếm Nhị Phân

Cây Tìm Kiếm Nhị Phân được sử dụng trong nhiều ứng dụng khác nhau, bao gồm:

Những Lưu Ý về Hiệu Năng

Hiệu năng của một BST phụ thuộc vào cấu trúc của nó. Trong trường hợp tốt nhất, một BST cân bằng cho phép độ phức tạp thời gian logarit cho các phép toán chèn, tìm kiếm và xóa. Tuy nhiên, trong trường hợp xấu nhất (ví dụ, một cây bị lệch), độ phức tạp thời gian có thể suy giảm xuống thời gian tuyến tính.

Cây Cân Bằng và Cây Không Cân Bằng

Một BST cân bằng là cây mà chiều cao của cây con trái và cây con phải của mọi nút chỉ chênh lệch nhau tối đa là một. Các thuật toán tự cân bằng, chẳng hạn như cây AVL và cây Đỏ-Đen, đảm bảo rằng cây luôn cân bằng, mang lại hiệu năng nhất quán. Các khu vực khác nhau có thể yêu cầu các mức độ tối ưu hóa khác nhau dựa trên tải của máy chủ; việc cân bằng giúp duy trì hiệu năng dưới mức sử dụng toàn cầu cao.

Độ Phức Tạp Thời Gian

Các Khái Niệm BST Nâng Cao

Cây Tự Cân Bằng

Cây tự cân bằng là các BST tự động điều chỉnh cấu trúc của chúng để duy trì sự cân bằng. Điều này đảm bảo rằng chiều cao của cây vẫn ở mức logarit, cung cấp hiệu năng nhất quán cho tất cả các hoạt động. Các cây tự cân bằng phổ biến bao gồm cây AVL và cây Đỏ-Đen.

Cây AVL

Cây AVL duy trì sự cân bằng bằng cách đảm bảo rằng sự chênh lệch chiều cao giữa cây con trái và cây con phải của bất kỳ nút nào cũng không quá một. Khi sự cân bằng này bị phá vỡ, các phép xoay được thực hiện để khôi phục lại sự cân bằng.

Cây Đỏ-Đen

Cây Đỏ-Đen sử dụng các thuộc tính màu (đỏ hoặc đen) để duy trì sự cân bằng. Chúng phức tạp hơn cây AVL nhưng mang lại hiệu năng tốt hơn trong một số kịch bản nhất định.

Ví Dụ Mã JavaScript: Triển Khai Cây Tìm Kiếm Nhị Phân Hoàn Chỉnh


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(key) {
    const newNode = new Node(key);

    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  search(key) {
    return this.searchNode(this.root, key);
  }

  searchNode(node, key) {
    if (node === null) {
      return false;
    }

    if (key < node.key) {
      return this.searchNode(node.left, key);
    } else if (key > node.key) {
      return this.searchNode(node.right, key);
    } else {
      return true;
    }
  }

  remove(key) {
    this.root = this.removeNode(this.root, key);
  }

  removeNode(node, key) {
    if (node === null) {
      return null;
    }

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      // khóa bằng với khóa của nút

      // trường hợp 1 - một nút lá
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // trường hợp 2 - nút chỉ có 1 con
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // trường hợp 3 - nút có 2 con
      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }

  findMinNode(node) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  min() {
    return this.minNode(this.root);
  }

  minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }

  max() {
    return this.maxNode(this.root);
  }

  maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current;
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }

  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

// Ví dụ sử dụng
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

const printNode = (value) => console.log(value);

console.log("Duyệt theo thứ tự trong:");
bst.inOrderTraverse(printNode);

console.log("Duyệt theo thứ tự trước:");
bst.preOrderTraverse(printNode);

console.log("Duyệt theo thứ tự sau:");
bst.postOrderTraverse(printNode);

console.log("Giá trị nhỏ nhất:", bst.min().key);
console.log("Giá trị lớn nhất:", bst.max().key);

console.log("Tìm kiếm số 9:", bst.search(9));
console.log("Tìm kiếm số 2:", bst.search(2));

bst.remove(7);
console.log("Tìm kiếm số 7 sau khi xóa:", bst.search(7));

Kết Luận

Cây Tìm Kiếm Nhị Phân là một cấu trúc dữ liệu mạnh mẽ và linh hoạt với vô số ứng dụng. Hướng dẫn này đã cung cấp một cái nhìn tổng quan toàn diện về BST, bao gồm cấu trúc, các phép toán và cách triển khai bằng JavaScript. Bằng cách hiểu các nguyên tắc và kỹ thuật được thảo luận trong hướng dẫn này, các nhà phát triển trên toàn thế giới có thể sử dụng BST một cách hiệu quả để giải quyết một loạt các vấn đề trong phát triển phần mềm. Từ việc quản lý cơ sở dữ liệu toàn cầu đến tối ưu hóa các thuật toán tìm kiếm, kiến thức về BST là một tài sản vô giá đối với bất kỳ lập trình viên nào.

Khi bạn tiếp tục hành trình trong khoa học máy tính, việc khám phá các khái niệm nâng cao như cây tự cân bằng và các cách triển khai khác nhau của chúng sẽ nâng cao hơn nữa sự hiểu biết và khả năng của bạn. Hãy tiếp tục thực hành và thử nghiệm với các kịch bản khác nhau để làm chủ nghệ thuật sử dụng Cây Tìm Kiếm Nhị Phân một cách hiệu quả.